/*
 * SoapUI, Copyright (C) 2004-2022 SmartBear Software
 *
 * Licensed under the EUPL, Version 1.1 or - as soon as they will be approved by the European Commission - subsequent 
 * versions of the EUPL (the "Licence"); 
 * You may not use this work except in compliance with the Licence. 
 * You may obtain a copy of the Licence at: 
 * 
 * http://ec.europa.eu/idabc/eupl 
 * 
 * Unless required by applicable law or agreed to in writing, software distributed under the Licence is 
 * distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 
 * express or implied. See the Licence for the specific language governing permissions and limitations 
 * under the Licence. 
 */

package com.eviware.soapui.tools;

import com.eviware.soapui.SoapUI;
import com.eviware.soapui.analytics.Analytics;
import com.eviware.soapui.analytics.AnalyticsHelper;
import com.eviware.soapui.impl.wsdl.WsdlProject;
import com.eviware.soapui.impl.wsdl.WsdlTestSuite;
import com.eviware.soapui.impl.wsdl.testcase.WsdlTestCase;
import com.eviware.soapui.impl.wsdl.testcase.WsdlTestCaseRunner;
import com.eviware.soapui.impl.wsdl.teststeps.WsdlRunTestCaseTestStep;
import com.eviware.soapui.model.iface.Attachment;
import com.eviware.soapui.model.iface.MessageExchange;
import com.eviware.soapui.model.project.ProjectFactoryRegistry;
import com.eviware.soapui.model.security.SecurityScan;
import com.eviware.soapui.model.support.ModelSupport;
import com.eviware.soapui.model.testsuite.TestCase;
import com.eviware.soapui.model.testsuite.TestCaseRunContext;
import com.eviware.soapui.model.testsuite.TestCaseRunner;
import com.eviware.soapui.model.testsuite.TestStep;
import com.eviware.soapui.model.testsuite.TestStepResult;
import com.eviware.soapui.model.testsuite.TestSuite;
import com.eviware.soapui.report.JUnitSecurityReportCollector;
import com.eviware.soapui.security.SecurityTest;
import com.eviware.soapui.security.SecurityTestRunContext;
import com.eviware.soapui.security.SecurityTestRunner;
import com.eviware.soapui.security.result.SecurityResult;
import com.eviware.soapui.security.result.SecurityResult.ResultStatus;
import com.eviware.soapui.security.result.SecurityScanRequestResult;
import com.eviware.soapui.security.result.SecurityScanResult;
import com.eviware.soapui.security.result.SecurityTestStepResult;
import com.eviware.soapui.security.support.SecurityTestRunListener;
import com.eviware.soapui.security.support.SecurityTestRunListenerAdapter;
import com.eviware.soapui.support.StringUtils;
import com.eviware.soapui.support.Tools;
import org.apache.commons.cli.CommandLine;

import java.io.File;
import java.io.FileOutputStream;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;

import static com.eviware.soapui.analytics.SoapUIActions.LAUNCH_SECURITY_TEST_RUNNER;
import static com.eviware.soapui.analytics.SoapUIActions.LAUNCH_SECURITY_TEST_RUNNER_FROM_UI;
import static com.eviware.soapui.impl.wsdl.actions.iface.tools.support.ProcessToolRunner.STARTED_FROM_GUI;

/**
 * Standalone security test-runner used from maven-plugin, can also be used from
 * command-line (see xdocs) or directly from other classes.
 * <p>
 * For standalone usage, set the project file (with setProjectFile) and other
 * desired properties before calling run
 * </p>
 *
 * @author nebojsa.tasic
 */

public class SoapUISecurityTestRunner extends SoapUITestCaseRunner implements SecurityTestRunListener {
    public static final String SOAPUI_EXPORT_SEPARATOR = "soapui.export.separator";

    public static final String TITLE = "SoapUI " + SoapUI.SOAPUI_VERSION + " Security Test Runner";
    private String securityTestName;
    private int securityTestCount;
    private int securityScanCount;
    private int securityScanRequestCount;
    private int securityScanAlertCount;
    private List<SecurityTestStepResult> failedResults = new ArrayList<>();
    private JUnitSecurityReportCollector reportCollector = new JUnitSecurityReportCollector();

    /**
     * Runs the tests in the specified soapUI project file, see SoapUI xdocs for
     * details.
     *
     * @param args
     * @throws Exception
     */

    public static void main(String[] args) throws Exception {
        System.exit(new SoapUISecurityTestRunner().runFromCommandLine(args));
    }

    protected boolean processCommandLine(CommandLine cmd) {
        if (cmd.hasOption("n")) {
            setSecurityTestName(cmd.getOptionValue("n"));
        }

        return super.processCommandLine(cmd);
    }

    public void setSecurityTestName(String securityTestName) {
        this.securityTestName = securityTestName;
    }

    protected SoapUIOptions initCommandLineOptions() {
        SoapUIOptions options = super.initCommandLineOptions();
        options.addOption("n", true, "Sets the security test name");

        return options;
    }

    public SoapUISecurityTestRunner() {
        super(SoapUISecurityTestRunner.TITLE);
    }

    public SoapUISecurityTestRunner(String title) {
        super(title);
    }

    public boolean runRunner() throws Exception {
        AnalyticsHelper.initializeAnalytics();
        Analytics.trackSessionStart();
        if (System.getenv(STARTED_FROM_GUI) == null) {
            Analytics.trackAction(LAUNCH_SECURITY_TEST_RUNNER);
        } else {
            Analytics.trackAction(LAUNCH_SECURITY_TEST_RUNNER_FROM_UI);
        }

        initGroovyLog();
        getAssertions().clear();
        String projectFile = getProjectFile();

        WsdlProject project = (WsdlProject) ProjectFactoryRegistry.getProjectFactory("wsdl").createNew(projectFile,
                getProjectPassword());

        if (project.isDisabled()) {
            throw new Exception("Failed to load SoapUI project file [" + projectFile + "]");
        }

        initProject(project);
        ensureOutputFolder(project);

        log.info("Running SoapUI tests in project [" + project.getName() + "]");

        String testSuite = getTestSuite();
        String testCase = getTestCase();

        long startTime = System.nanoTime();

        List<TestCase> testCasesToRun = new ArrayList<>();

        // start by listening to all testcases.. (since one testcase can call
        // another)
        for (int c = 0; c < project.getTestSuiteCount(); c++) {
            TestSuite suite = project.getTestSuiteAt(c);
            for (int i = 0; i < suite.getTestCaseCount(); i++) {
                TestCase tc = suite.getTestCaseAt(i);
                if ((testSuite == null || suite.getName().equals(suite.getName())) && testCase != null
                        && tc.getName().equals(testCase)) {
                    testCasesToRun.add(tc);
                }

                addListeners(tc);
            }
        }

        // decide what to run
        if (testCasesToRun.size() > 0) {
            for (TestCase tc : testCasesToRun) {
                runTestCase((WsdlTestCase) tc);
            }
        } else if (testSuite != null) {
            WsdlTestSuite ts = project.getTestSuiteByName(testSuite);
            if (ts == null) {
                throw new Exception("TestSuite with name [" + testSuite + "] not found in project");
            } else {
                runSuite(ts);
            }
        } else {
            runProject(project);
        }

        long timeTaken = (System.nanoTime() - startTime) / 1000000;

        if (isPrintReport()) {
            printReport(timeTaken);
        }

        exportReports(project);

        if (isSaveAfterRun() && !project.isRemote()) {
            try {
                project.save();
            } catch (Throwable t) {
                log.error("Failed to save project", t);
            }
        }

        if (securityScanAlertCount > 0 && !isIgnoreErrors()) {
            throw new Exception("SecurityTest execution failed with " + securityScanAlertCount + " alert"
                    + (securityScanAlertCount > 1 ? "s" : ""));
        }

        return true;
    }

    protected void runProject(WsdlProject project) {
        try {
            log.info(("Running Project [" + project.getName() + "], runType = " + project.getRunType()));
            for (TestSuite testSuite : project.getTestSuiteList()) {
                runSuite((WsdlTestSuite) testSuite);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    protected void initProject(WsdlProject project) throws Exception {
        initProjectProperties(project);
    }

    protected void exportReports(WsdlProject project) throws Exception {
        if (isJUnitReport()) {
            exportJUnitReports(reportCollector, getAbsoluteOutputFolder(project), project);
        }
    }

    protected void addListeners(TestCase tc) {
        tc.addTestRunListener(this);
        if (isJunitReport()) {
            tc.addTestRunListener(reportCollector);
        }
    }

    public void exportJUnitReports(JUnitSecurityReportCollector collector, String folder, WsdlProject project)
            throws Exception {
        collector.saveReports(folder == null ? "" : folder);
    }

    public void printReport(long timeTaken) {
        System.out.println();
        System.out.println("SoapUI " + SoapUI.SOAPUI_VERSION + " Security TestCaseRunner Summary");
        System.out.println("-----------------------------");
        System.out.println("Time Taken: " + timeTaken + "ms");
        System.out.println("Total SecurityTests: " + securityTestCount);
        System.out.println("Total SecurityScans: " + securityScanCount);
        System.out.println("Total SecurityScan Requests: " + securityScanRequestCount);
        System.out.println("Total Failed SecurityScan Requests: " + securityScanAlertCount);
    }

    /**
     * Run tests in the specified TestSuite
     *
     * @param suite the TestSuite to run
     */

    protected void runSuite(WsdlTestSuite suite) {
        try {
            for (TestCase testCase : suite.getTestCaseList()) {
                runTestCase((WsdlTestCase) testCase);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

    }

    /**
     * Runs the SecurityTests in the specified TestCase
     *
     * @param testCase the testcase to run
     * @param context
     */

    protected void runTestCase(WsdlTestCase testCase) {
        try {
            for (SecurityTest securityTest : testCase.getSecurityTestList()) {

                securityTest.addSecurityTestRunListener(this);

                if (StringUtils.isNullOrEmpty(securityTestName) || securityTest.getName().equals(securityTestName)) {
                    runSecurityTest(securityTest);
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * @param securityTest
     */
    protected void runSecurityTest(SecurityTest securityTest) {
        securityTest.addSecurityTestRunListener(new SecurityTestRunListenerAdapter() {
            private int requestIndex = 0;

            @Override
            public void afterSecurityScanRequest(TestCaseRunner testRunner, SecurityTestRunContext runContext,
                                                 SecurityScanRequestResult securityCheckReqResult) {
                securityScanRequestCount++;
                if (securityCheckReqResult.getStatus() == ResultStatus.FAILED) {
                    securityScanAlertCount++;
                }

                log.info(securityCheckReqResult.getSecurityScan().getName() + " - "
                        + securityCheckReqResult.getChangedParamsInfo(++requestIndex));
            }

            @Override
            public void afterSecurityScan(TestCaseRunner testRunner, SecurityTestRunContext runContext,
                                          SecurityScanResult securityCheckResult) {
                securityScanCount++;
            }

            @Override
            public void beforeSecurityScan(TestCaseRunner testRunner, SecurityTestRunContext runContext,
                                           SecurityScan securityCheck) {
                requestIndex = 0;
            }

            @Override
            public void afterStep(TestCaseRunner testRunner, SecurityTestRunContext runContext,
                                  SecurityTestStepResult result) {
                if (result.getStatus() == ResultStatus.FAILED) {
                    failedResults.add(result);
                }
            }
        });

        if (isJUnitReport()) {
            securityTest.addSecurityTestRunListener(reportCollector);
        }

        log.info("Running SecurityTest [" + securityTest.getName() + "] in TestCase ["
                + securityTest.getTestCase().getName() + "] in TestSuite ["
                + securityTest.getTestCase().getTestSuite().getName() + "]");

        SecurityTestRunner runner = securityTest.run(null, false);
        // log.info( "\n" + securityTest.getSecurityTestLog().getMessages() );
        log.info("SecurityTest [" + securityTest.getName() + "] finished with status [" + runner.getStatus() + "] in "
                + (runner.getTimeTaken()) + "ms");

        if (isJUnitReport()) {
            securityTest.removeSecurityTestRunListener(reportCollector);
        }
    }

    @Override
    public void afterStep(TestCaseRunner testRunner, SecurityTestRunContext runContext, SecurityTestStepResult result) {
        if (!isPrintReport()) {
            return;
        }

        TestStep currentStep = runContext.getCurrentStep();

        String securityTestName = "";
        String securityScanName = "";
        if (!result.getSecurityScanResultList().isEmpty()) {
            securityTestName = result.getSecurityScanResultList().get(0).getSecurityScan().getParent().getName();
            securityScanName = result.getSecurityScanResultList().get(0).getSecurityScanName();
        }

        String countPropertyName = currentStep.getName() + " run count";
        Long count = new Long(getExportCount());// ( Long
        // )runContext.getProperty(
        // countPropertyName );
        if (count == null) {
            count = new Long(0);
        }

        runContext.setProperty(countPropertyName, new Long(count.longValue() + 1));

        if (result.getStatus() == SecurityResult.ResultStatus.FAILED || isExportAll()) {
            try {
                String exportSeparator = System.getProperty(SOAPUI_EXPORT_SEPARATOR, "-");

                TestCase tc = currentStep.getTestCase();

                String nameBase = StringUtils.createFileName(securityTestName, '_') + exportSeparator
                        + StringUtils.createFileName(securityScanName, '_') + exportSeparator
                        + StringUtils.createFileName(tc.getTestSuite().getName(), '_') + exportSeparator
                        + StringUtils.createFileName(tc.getName(), '_') + exportSeparator
                        + StringUtils.createFileName(currentStep.getName(), '_') + "-" + count.longValue() + "-"
                        + result.getStatus();

                WsdlTestCaseRunner callingTestCaseRunner = (WsdlTestCaseRunner) runContext
                        .getProperty("#CallingTestCaseRunner#");

                if (callingTestCaseRunner != null) {
                    WsdlTestCase ctc = callingTestCaseRunner.getTestCase();
                    WsdlRunTestCaseTestStep runTestCaseTestStep = (WsdlRunTestCaseTestStep) runContext
                            .getProperty("#CallingRunTestCaseStep#");

                    nameBase = StringUtils.createFileName(securityTestName, '_') + exportSeparator
                            + StringUtils.createFileName(ctc.getTestSuite().getName(), '_') + exportSeparator
                            + StringUtils.createFileName(ctc.getName(), '_') + exportSeparator
                            + StringUtils.createFileName(runTestCaseTestStep.getName(), '_') + exportSeparator
                            + StringUtils.createFileName(tc.getTestSuite().getName(), '_') + exportSeparator
                            + StringUtils.createFileName(tc.getName(), '_') + exportSeparator
                            + StringUtils.createFileName(currentStep.getName(), '_') + "-" + count.longValue() + "-"
                            + result.getStatus();
                }

                String absoluteOutputFolder = getAbsoluteOutputFolder(ModelSupport.getModelItemProject(tc));
                String fileName = absoluteOutputFolder + File.separator + nameBase + ".txt";

                if (result.getStatus() == SecurityResult.ResultStatus.FAILED) {
                    log.error(currentStep.getName() + " failed, exporting to [" + fileName + "]");
                }

                File file = new File(fileName);
                file.getParentFile().mkdirs();

                PrintWriter writer = new PrintWriter(file);
                result.writeTo(writer);
                writer.close();

                // write attachments
                if (result instanceof MessageExchange) {
                    Attachment[] attachments = ((MessageExchange) result).getResponseAttachments();
                    if (attachments != null && attachments.length > 0) {
                        for (int c = 0; c < attachments.length; c++) {
                            fileName = nameBase + "-attachment-" + (c + 1) + ".";

                            Attachment attachment = attachments[c];
                            String contentType = attachment.getContentType();
                            if (!"application/octet-stream".equals(contentType) && contentType != null
                                    && contentType.indexOf('/') != -1) {
                                fileName += contentType.substring(contentType.lastIndexOf('/') + 1);
                            } else {
                                fileName += "dat";
                            }

                            fileName = absoluteOutputFolder + File.separator + fileName;

                            FileOutputStream outFile = new FileOutputStream(fileName);
                            Tools.writeAll(outFile, attachment.getInputStream());
                            outFile.close();
                        }
                    }
                }

                setExportCount(getExportCount() + 1);
            } catch (Exception e) {
                log.error("Error saving failed result: " + e, e);
            }
        }

        setTestStepCount(getTestStepCount() + 1);

    }

    public void afterRun(TestCaseRunner testRunner, TestCaseRunContext runContext) {
    }

    @Override
    public void afterOriginalStep(TestCaseRunner testRunner, SecurityTestRunContext runContext,
                                  SecurityTestStepResult result) {
        // TODO Auto-generated method stub

    }

    @Override
    public void afterRun(TestCaseRunner testRunner, SecurityTestRunContext runContext) {
        // TODO Auto-generated method stub

    }

    @Override
    public void afterSecurityScan(TestCaseRunner testRunner, SecurityTestRunContext runContext,
                                  SecurityScanResult securityScanResult) {
        // TODO Auto-generated method stub

    }

    @Override
    public void afterSecurityScanRequest(TestCaseRunner testRunner, SecurityTestRunContext runContext,
                                         SecurityScanRequestResult securityScanReqResult) {
        // TODO Auto-generated method stub

    }

    @Override
    public void beforeRun(TestCaseRunner testRunner, SecurityTestRunContext runContext) {
        // TODO Auto-generated method stub

    }

    @Override
    public void beforeSecurityScan(TestCaseRunner testRunner, SecurityTestRunContext runContext,
                                   SecurityScan securityScan) {
        // TODO Auto-generated method stub

    }

    @Override
    public void beforeStep(TestCaseRunner testRunner, SecurityTestRunContext runContext, TestStepResult testStepResult) {
        // TODO Auto-generated method stub

    }

}
